package cheesymock.recorder;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
 * An expect recorder for a mock proxy. This class will just record method invocations to a queue.
 *
 * @author Bjorn
 *
 */
public class RecorderMock implements InvocationHandler, MethodInterceptor {
	private final Object originalProxy;
	private final RecorderState recorderState;
	private final RecorderErrorMessageFactory messageFactory;

	public RecorderMock(Object proxy, RecorderState recorderState, RecorderErrorMessageFactory messageFactory) {
		originalProxy = proxy;
		this.recorderState = recorderState;
		this.messageFactory = messageFactory;
	}

	/**
	 * CGLib, forward call to invoke-method
	 */
	public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodproxy) throws Throwable {
		return invoke(proxy, method, args);
	}

	/**
	 * A expected method call has been executed on the RecordMock.
	 *
	 */
	public Object invoke(Object recorderProxy, Method method, Object[] args) throws Throwable {
		final Class<?>[] parameterTypes = method.getParameterTypes();
		if (parameterTypes.length == 0 && args == null) {
			args = new Object[0];
		}
		final MethodInvocation methodInvocation = new MethodInvocation(originalProxy, method, args);
		recorderState.addExpectedInvocation(methodInvocation);
		return null;
	}

	/**
	 * Check actual method invocation against the stored expected method call.
	 *
	 * @param proxy
	 * @param method
	 * @param args
	 */
	public void invoked(Object proxy, Method method, Object[] args) {
		final MethodInvocation expectedMethodInvocation = recorderState.getExpectedInvocationsQueue().poll();
		final Class<?>[] parameterTypes = method.getParameterTypes();
		if (parameterTypes.length == 0 && args == null) {
			args = new Object[0];
		}
		if (args == null) {
			args = new Object[0];
		}
		final MethodInvocation currentMethodInvocation = new MethodInvocation(proxy, method, args);
		checkNoExpectationMessage(expectedMethodInvocation, currentMethodInvocation);
		checkMethodMissmatchMessage(expectedMethodInvocation, currentMethodInvocation);
		checkMethodArgumentMissmatch(expectedMethodInvocation, currentMethodInvocation);
		checkNotSameProxy(proxy, expectedMethodInvocation, currentMethodInvocation);
		recorderState.getCorrectInvocationsQueue().add(expectedMethodInvocation);
	}

	private void checkMethodArgumentMissmatch(final MethodInvocation expectedMethodInvocation,
			final MethodInvocation currentMethodInvocation) {
		final Object[] expectedArguments = expectedMethodInvocation.getArgs();
		final Object[] actualArguments = currentMethodInvocation.getArgs();
		Set<Integer> argumentMissmatcheIndexes = new TreeSet<Integer>();
		for (int i = 0; i < expectedArguments.length; i++) {
			Object expectedArgument = expectedArguments[i];
			Object actualArgument = actualArguments[i];
			final ArgumentModifier<? extends Object> argumentModifier = expectedMethodInvocation.getArgumentModifier(i);
			if (argumentModifier != null) {
				try {
					if (!argumentModifier.equalArgument(actualArgument)) {
						argumentMissmatcheIndexes.add(i);
					}
				} catch (ClassCastException ccex) {
					throw new RecorderAssertionError(messageFactory.createArgumentModifierIncorrectClass(recorderState
							.getCorrectInvocationsQueue(), expectedMethodInvocation, currentMethodInvocation, i), ccex);
				}
			} else {
				// it is the recorders argument that decides if arguments are
				// equal or not
				if (expectedArgument == null) {
					if (actualArgument != null) {
						argumentMissmatcheIndexes.add(i);
					}
				} else {
					if (!expectedArgument.equals(actualArgument)) {
						argumentMissmatcheIndexes.add(i);
					}
				}
			}
		}
		if (!argumentMissmatcheIndexes.isEmpty()) {
			throw new RecorderAssertionError(messageFactory.createMethodArgumentMissmatch(recorderState
					.getCorrectInvocationsQueue(), expectedMethodInvocation, currentMethodInvocation,
					argumentMissmatcheIndexes));
		}
	}

	private void checkMethodMissmatchMessage(final MethodInvocation expectedMethodInvocation,
			final MethodInvocation currentMethodInvocation) {
		if (!expectedMethodInvocation.getMethod().equals(currentMethodInvocation.getMethod())) {
			throw new RecorderAssertionError(messageFactory.createMethodMissmatchMessage(recorderState
					.getCorrectInvocationsQueue(), expectedMethodInvocation, currentMethodInvocation));
		}
	}

	private void checkNoExpectationMessage(final MethodInvocation expectedMethodInvocation,
			final MethodInvocation currentMethodInvocation) {
		if (expectedMethodInvocation == null)
			throw new RecorderAssertionError(messageFactory.createNoExpectationMessage(recorderState
					.getCorrectInvocationsQueue(), currentMethodInvocation));
	}

	private void checkNotSameProxy(Object proxy, final MethodInvocation expectedMethodInvocation,
			final MethodInvocation currentMethodInvocation) {
		boolean same = expectedMethodInvocation.getOriginalProxy() == proxy;
		if (!same)
			throw new RecorderAssertionError(messageFactory.createNotSameProxyMessage(recorderState
					.getCorrectInvocationsQueue(), expectedMethodInvocation, currentMethodInvocation));
	}
}

class MethodInvocation {
	private final Object originalProxy;
	private final Method method;
	private final Object[] args;
	private final List<ArgumentModifier<? extends Object>> argumentModifiers = new ArrayList<ArgumentModifier<? extends Object>>();

	public MethodInvocation(Object originalProxy, Method method, Object[] args) {
		this.originalProxy = originalProxy;
		this.method = method;
		this.args = args;
	}

	public Object[] getArgs() {
		return args;
	}

	public ArgumentModifier<? extends Object> getArgumentModifier(int index) {
		for (ArgumentModifier<? extends Object> argumentModifier : argumentModifiers) {
			if (argumentModifier.getArgumentIndex() == index)
				return argumentModifier;
		}
		return null;
	}

	public List<ArgumentModifier<? extends Object>> getArgumentModifiers() {
		return argumentModifiers;
	}

	public Method getMethod() {
		return method;
	}

	public Object getOriginalProxy() {
		return originalProxy;
	}
}
